﻿using System;
using System.Text.RegularExpressions;
using System.Web;
using Birkelund.QueryStrings.Internals;

namespace Birkelund.QueryStrings.Tokens
{
    /// <summary>
    /// The baseclass for querystring tokens which provides common functionality
    /// </summary>
    /// <typeparam name="T">Type of token</typeparam>
    public abstract class QueryStringTokenBase<T> : IQueryStringToken<T>
    {
        #region Properties
        /// <summary>
        /// Gets or sets the delegate that is called if the token parser discovers an error. 
        /// Is called before any exception is thrown: see ErrorNotificationBehaviour
        /// </summary>
        /// <value></value>
        public Action<FailureArgs> ReadFailureAction { get; set; }
        /// <summary>
        /// Gets the unique key used as identifier
        /// </summary>
        /// <value></value>
        public string KeyName { get; private set; }
        /// <summary>
        /// Gets or sets the value to return if any errors occurs or if null/no value is found
        /// </summary>
        /// <value></value>
        public T DefaultValue { get; set; }
        /// <summary>
        /// Gets or sets the validator. If validation fails, the token parser will enter errormode and call the delegate speficied in <see cref="ReadFailureAction"/>
        /// </summary>
        /// <value>The validator.</value>
        public Func<T, bool> Validator { get; set; }
        /// <summary>
        /// Gets or sets the ExceptionNotificationBehaviour.
        /// </summary>
        /// <value>The value to get or set.</value>
        public ExceptionNotificationBehaviour ExceptionNotificationBehaviour { get; set; }

        /// <summary>
        /// Get or sets the encryption algorithm. If null (default) encryption will not be used.
        /// </summary>
        /// <value>The value to get or set.</value>
        public ISymmetricEncryption EncryptionAlgorithm { get; set; }
        #endregion

        private static readonly Regex validKeyExpression = new Regex(@"^[A-Za-z]+$", RegexOptions.Compiled);
        /// <summary>
        /// Max allowed langth of key
        /// </summary>
        protected const int MAX_KEY_LENGTH = 10;

        /// <summary>
        /// Initializes a new instance of the <see cref="QueryStringTokenBase&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="keyName">The key.</param>
        protected QueryStringTokenBase(string keyName)
            : this(keyName, true)
        {
        }

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="keyName">The key</param>
        /// <param name="doKeyValidation">False to bypass key validation</param>
        internal protected QueryStringTokenBase(string keyName, bool doKeyValidation)
        {
            if (doKeyValidation)
            {
                ValidateKeyName(keyName);
            }

            KeyName = keyName;
            DefaultValue = default(T);
            ExceptionNotificationBehaviour = ExceptionNotificationBehaviour.ThrowException;
        }

        #region Public Methods
        /// <summary>
        /// Reads the value from the specified querystring
        /// </summary>
        /// <param name="uri">The URL.</param>
        /// <param name="readmode">The readmode to apply</param>
        /// <returns>
        /// Returns the read value.
        /// If the value cannot be passed to currentType T or if the value doesn't exist the default value will be returned.
        /// </returns>
        public T ReadValue(Uri uri, ReadValueMode readmode)
        {
            T typedValue;
            TryReadValueWithNotification(uri, out typedValue, readmode, true); // we don't care about the return value of this TryReadValue-method
            return typedValue;
        }

        /// <summary>
        /// Reads the value from the current querystring
        /// </summary>
        /// <returns>
        /// Returns the read value.
        /// If the value cannot be passed to currentType T or if the value doesn't exist the default value will be returned.
        /// </returns>
        public T ReadValue()
        {
            return ReadValue(ReadValueMode.NoCheckSumControl);
        }

        /// <summary>
        /// Reads the value from the current querystring
        /// </summary>
        /// <param name="readmode">The readmode to apply. See IUriWriter.ApplyCheckSum</param>
        /// <returns>
        /// Returns the read value.
        /// If the value cannot be passed to currentType T or if the value doesn't exist the default value will be returned.
        /// </returns>
        public T ReadValue(ReadValueMode readmode)
        {
            return ReadValue(Common.CurrentUrl, readmode);
        }

        /// <summary>
        /// Reads the value from the specified querystring
        /// </summary>
        /// <param name="uri">The uri to determine</param>
        /// <returns>
        /// Returns the read value.
        /// If the value cannot be passed to currentType T or if the value doesn't exist the default value will be returned.
        /// </returns>
        public T ReadValue(Uri uri)
        {
            return ReadValue(uri, ReadValueMode.NoCheckSumControl);
        }

        /// <summary>
        /// Tries to read the value./>
        /// </summary>
        /// <param name="result">The result.</param>
        /// <returns>True or false</returns>
        public bool TryReadValue(out T result)
        {
            return TryReadValue(Common.CurrentUrl, out result, ReadValueMode.NoCheckSumControl);
        }

        /// <summary>
        /// Tries to read the value.
        /// </summary>
        /// <param name="result">The result.</param>
        /// <param name="readmode">The readmode to apply. See UrlWriter.ApplyCheckSum</param>
        /// <returns>True or false</returns>
        public bool TryReadValue(out T result, ReadValueMode readmode)
        {
            return TryReadValue(Common.CurrentUrl, out result, readmode);
        }

        /// <summary>
        /// Tries to read the value.
        /// </summary>
        /// <param name="uri">The uri</param>
        /// <param name="result">The result.</param>
        /// <param name="readmode">The readmode to apply. See UrlWriter.ApplyCheckSum</param>
        /// <returns>True or false</returns>
        /// <example sourceFile="..\..\QueryStrings\Documents\SampleCode\Demonstration.cs" regionName="Read a simple QueryString token" />
        public bool TryReadValue(Uri uri, out T result, ReadValueMode readmode)
        {
            return TryReadValueWithNotification(uri, out result, readmode, false);
        }

        /// <summary>
        /// Renders the token value
        /// </summary>
        /// <returns>
        /// Returns the rendered value. Intented to be used by the framework internal
        /// </returns>
        public string RenderTokenValue(T value)
        {
            string stringValue = Convert.ToString(OnRenderTokenValue(value));
            if (EncryptionAlgorithm != null)
            {
                stringValue = EncryptionAlgorithm.Encrypt(stringValue);
            }
            string encoded = HttpUtility.UrlEncode(stringValue);
            return encoded;
        }

        protected abstract string OnRenderTokenValue(T value);

        /// <summary>
        /// Checks whether the current token exists in the current QueryString
        /// </summary>
        /// <returns>True or false</returns>
        public bool Exists()
        {
            return Exists(Common.CurrentUrl);
        }

        /// <summary>
        /// Checks whether the current token exists in the specified url
        /// </summary>
        /// <param name="url">The url to determine</param>
        /// <returns>True or false</returns>
        public bool Exists(Uri url)
        {
            try
            {
                QueryStringReader reader = GetReader(url);
                bool doesExist = reader.Exists(this);
                return doesExist;
            }
            catch (TokenParserException ex)
            {
                FailureArgs args = CreateFailureArgs(url, null, ex.FailureArgs.Reason, ex);
                Notify(args);
                return false;
            }
        }

        /// <summary>
        /// Returns a <see cref="System.String"/> that represents this instance.
        /// </summary>
        /// <returns>
        /// A <see cref="System.String"/> that represents this instance.
        /// </returns>
        public override string ToString()
        {
            return string.Format("KeyName: '{0}', Type: '{1}'", KeyName, GetType().Name);
        }
        #endregion

        #region Private Methods
        private bool TryReadValueWithNotification(Uri uri, out T result, ReadValueMode readmode, bool throwExceptionOnErrorIfAllowed)
        {
            FailureArgs args = TryReadValueImpl(uri, out result, readmode);
            if (args == null)
            {
                return true;
            }
            else
            {
                Notify(args, throwExceptionOnErrorIfAllowed);
                return false;
            }
        }

        private FailureArgs TryReadValueImpl(Uri uri, out T result, ReadValueMode readmode)
        {
            QueryStringReader reader = null;
            try
            {
                reader = GetReader(uri);
            }
            catch (TokenParserException ex)
            {
                result = DefaultValue;
                return CreateFailureArgs(uri, null, ex.FailureArgs.Reason, ex);
            }

            if (!reader.Exists(this))
            {
                result = DefaultValue;
                return CreateFailureArgs(uri, null, FailureReason.TokenDoesNotExist);
            }

            #region Parsing

            string attemptedValue = null;
            T typedValue;
            try
            {
                attemptedValue = reader.ReadStringValue(this, UriEncodingMode.HandleEncoding);
                if (EncryptionAlgorithm != null)
                {
                    attemptedValue = EncryptionAlgorithm.Decrypt(attemptedValue);
                }

                typedValue = OnParseFromString(attemptedValue);
            }
            catch (Exception ex)
            {
                result = DefaultValue;
                return CreateFailureArgs(uri, attemptedValue, FailureReason.ParseError, ex);
            }
            #endregion

            #region CheckSum control
            if (readmode == ReadValueMode.CheckSumControl)
            {
                ArgumentValidation.ThrowIfNull(QueryStringConfiguration.Instance.ChecksumCalculator, "ChecksumCalculator is not set. Apply on QueryStringConfiguration");

                ITokenKey checkSum = CheckSumKeyToken.Instance;
                if (!reader.Exists(checkSum))
                {
                    result = DefaultValue;
                    return CreateFailureArgs(uri, attemptedValue, FailureReason.CheckSumTokenDoesNotExist);
                }
                string calculatedCheckSum = QueryStringConfiguration.Instance.ChecksumCalculator.CalculateCheckSum(reader.Tokens.GetTokensWhichAreNotChecksumToken());
                string actualCheckSum = reader.ReadStringValue(checkSum, UriEncodingMode.HandleEncoding);
                if (!Common.AreEqual(calculatedCheckSum, actualCheckSum, true))
                {
                    result = DefaultValue;
                    return CreateFailureArgs(uri, attemptedValue, FailureReason.CheckSumError);
                }
            }
            #endregion

            #region Validating
            bool isValid = (Validator == null || Validator(typedValue));

            if (isValid)
            {
                result = typedValue;
                return null;// This is the only point where this method is succesfull completed
            }
            else
            {
                result = DefaultValue;
                return CreateFailureArgs(uri, attemptedValue, FailureReason.ValidationError);
            }
            #endregion
        }

        /// <summary>
        /// Create an instance of the QueryStringReader
        /// </summary>
        /// <param name="uri">The url to create the reader on</param>
        /// <returns></returns>
        /// <exception cref="TokenParserException">If validation of querystring fails</exception>
        private QueryStringReader GetReader(Uri uri)
        {
            return QueryStringReader.CreateFromUrl(uri);
        }

        private FailureArgs CreateFailureArgs(Uri fullUrl, string attemptedValue, FailureReason reason, Exception ex = null)
        {
            FailureArgs args = new FailureArgs(KeyName, attemptedValue, fullUrl, reason, typeof(T), ex);
            return args;
        }

        private void Notify(FailureArgs args, bool throwExceptionOnErrorIfAllowed = true)
        {
            ArgumentValidation.ThrowIfNull(args, "args");

            if (ReadFailureAction != null)
            {
                ReadFailureAction(args);
            }
            switch (ExceptionNotificationBehaviour)
            {
                case ExceptionNotificationBehaviour.Silent:
                    goto default;
                case ExceptionNotificationBehaviour.ThrowException:
                    {
                        if (throwExceptionOnErrorIfAllowed)
                        {
                            if (args.Exception != null && args.Exception.GetType() == typeof(TokenParserException))
                            {
                                throw args.Exception; //dont wrap in another TokenParserException
                            }
                            else
                            {
                                throw new TokenParserException(args);
                            }
                        }
                        else break;
                    }
                default:
                    break;
            }
        }
        #endregion

        #region Protected Methods
        /// <summary>
        /// ParseFromString the string value to a typed value. Implementation of this method MUST throw an exception on failure
        /// </summary>
        /// <param name="attemptedValue">The attempted value in clear text</param>
        /// <returns>Returns the strongly typed value</returns>
        protected abstract T OnParseFromString(string attemptedValue);

        /// <summary>
        /// Validates the key
        /// </summary>
        /// <param name="keyName">The keyname.</param>
        protected virtual void ValidateKeyName(string keyName)
        {
            ArgumentValidation.ThrowIfNull(keyName, "keyName");
            ArgumentValidation.ThrowIfLonger(keyName, MAX_KEY_LENGTH, "keyName");
            ArgumentValidation.ThrowIfNotMatch(keyName, validKeyExpression, "keyName");
            ArgumentValidation.ThrowIfEqual(keyName, CheckSumKeyToken.Instance.KeyName, "keyName"); // This keyname is reserved
        }
        #endregion
    }
}